package blockchain

import (
	"bytes"
	"crypto/ecdsa"
	"encoding/hex"
	"fmt"
	"goblockchain/constcoe"
	"goblockchain/transaction"
	"goblockchain/utils"
	"runtime"

	"github.com/dgraph-io/badger"
)

// 区块链就是区块的一个集合
type BlockChain struct {
	LastHash []byte     //指当前区块链最后一个区块的哈希值
	Database *badger.DB //指向存储区块的数据库
}

type BlockChainIterator struct {
	CurrentHash []byte
	Database    *badger.DB
}

// 创建迭代器的初始化函数
func (chain *BlockChain) Iterator() *BlockChainIterator {
	iterator := BlockChainIterator{chain.LastHash, chain.Database}
	return &iterator
}

// 创建迭代器的迭代函数，让每次迭代返回一个block，然后迭代器指向前一个区块的哈希值。
func (iterator *BlockChainIterator) Next() *Block {
	var block *Block

	err := iterator.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get(iterator.CurrentHash)
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			block = DeSerializeBlock(val)
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	iterator.CurrentHash = block.PrevHash
	return block
}

// 创建一个辅助函数来帮助判断迭代器是否终止
func (chain *BlockChain) BackOgPrevHash() []byte {
	var ogprevhash []byte
	err := chain.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("ogprevhash"))
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			ogprevhash = val
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	return ogprevhash
}

/*
构建函数，使得区块链可以根据其它信息创建区块进行储存。
AddBlock函数会先检查区块链中的LastHash与即将加入的区块的PrevHash是否一致，
如果一致才会将其加入到区块链（数据库）中，并更新数据库中的"lh"
*/
func (bc *BlockChain) AddBlock(newBlock *Block) {
	var lastHash []byte
	err := bc.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	if !bytes.Equal(newBlock.PrevHash, lastHash) {
		fmt.Println("This block is out of age")
		runtime.Goexit()
	}

	err = bc.Database.Update(func(transaction *badger.Txn) error {
		err := transaction.Set(newBlock.Hash, newBlock.Serialize())
		utils.Handle(err)
		err = transaction.Set([]byte("lh"), newBlock.Hash)
		bc.LastHash = newBlock.Hash
		return err
	})

	utils.Handle(err)
}

// 构建一个区块链初始化函数，使其返回一个包含创始区块的区块链
// func CreateBlockChain() *BlockChain {
// 	blockchain := BlockChain{}
// 	blockchain.Blocks = append(blockchain.Blocks, GenesisBlock())
// 	return &blockchain
// }

/*
InitBlockChain函数会先检查是否有存储区块链的数据库存在，如果存在将会给出警告并退出
（注意这里我们使用的是runtime.Goexit()进行退出,相对来说更加安全,当然你也可以使用一般
的退出方式）。opts就是启动Badger的配置，我们这里全部使用默认配置，
并将地址指定为constcoe.BCPath即可。opts.Logger =nil可以使数据库的操作信息不输出到标准输出中，
当然你也可以不进行该设置，方便在调用数据库时进行debug。badger.Open(opts)就是按照我们的配置启动一个数据库
（如果没有现成的数据库就会初始化一个），将会返回该数据库的指针。db.Update()是Badger中对数据库进行更新操作的函数，
*/
func InitBlockChain(address []byte) *BlockChain {
	var lastHash []byte

	if utils.FileExists(constcoe.BCFile) {
		fmt.Println("blockchain already exists")
		runtime.Goexit()
	}

	opts := badger.DefaultOptions(constcoe.BCPath)
	opts.Logger = nil

	db, err := badger.Open(opts)
	utils.Handle(err)
	//db.Update()更新数据库
	err = db.Update(func(txn *badger.Txn) error {
		genesis := GenesisBlock(address)
		fmt.Println("Genesis Created")
		err = txn.Set(genesis.Hash, genesis.Serialize())
		utils.Handle(err)
		err = txn.Set([]byte("lh"), genesis.Hash) //store the hash of the block in blockchain
		utils.Handle(err)
		err = txn.Set([]byte("ogprevhash"), genesis.PrevHash) //store the prevhash of genesis(original) block
		utils.Handle(err)
		lastHash = genesis.Hash
		return err
	})
	utils.Handle(err)
	blockchain := BlockChain{lastHash, db}
	return &blockchain

}

// Txn全称也就是Transaction
// 通过已有的数据库读取并加载我们的区块链
func ContinueBlockChain() *BlockChain {
	if utils.FileExists(constcoe.BCFile) == false {
		fmt.Println("No blockchain found, please create one first")
		runtime.Goexit()
	}
	var lastHash []byte

	opts := badger.DefaultOptions(constcoe.BCPath)
	opts.Logger = nil
	db, err := badger.Open(opts)
	utils.Handle(err)
	//db.View()调取视图
	err = db.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	chain := BlockChain{lastHash, db}
	return &chain

}

/*
这是一个返回区块链中一个地址的可用交易信息的函数。这里用到了go语言的label特性
unSpentTxs就是我们要返回包含指定地址的可用交易信息的切片。
spentTxs用于记录遍历区块链时那些已经被使用的交易信息的Output，
key值为交易信息的ID值（需要转成string），value值为Output在该交易信息中的序号。
利用FindUnspentTransactions函数，我们可以找到一个地址的所有UTXO以及该地址对应的资产总和。
*/
func (bc *BlockChain) FindUnspentTransactions(address []byte) []transaction.Transaction {

	var unSpentTxs []transaction.Transaction
	spentTxs := make(map[string][]int) // can't use type []byte as key value

	iter := bc.Iterator()
	//通过迭代器完成了对区块链中区块由后到前的遍历
all:
	for {
		block := iter.Next()

		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)

		IterOutputs:
			for outIdx, out := range tx.Outputs {
				if spentTxs[txID] != nil {
					for _, spentOut := range spentTxs[txID] {
						if spentOut == outIdx {
							continue IterOutputs
						}
					}
				}

				if out.ToAddressRight(address) {
					unSpentTxs = append(unSpentTxs, *tx)
				}
			}
			if !tx.IsBase() {
				for _, in := range tx.Inputs {
					if in.FromAddressRight(address) {
						inTxID := hex.EncodeToString(in.TxID)
						spentTxs[inTxID] = append(spentTxs[inTxID], in.OutIdx)
					}
				}
			}
		}
		if bytes.Equal(block.PrevHash, bc.BackOgPrevHash()) {
			break all
		}
	}

	// for idx := len(bc.Blocks) - 1; idx >= 0; idx-- {
	// 	block := bc.Blocks[idx]
	// 	for _, tx := range block.Transactions {
	// 		txID := hex.EncodeToString(tx.ID)
	// 		//遍历交易信息的Output，如果该Output在spentTxs中就跳过，说明该Output已被消费。
	// 	IterOutputs:
	// 		for outIdx, out := range tx.Outputs {
	// 			if spentTxs[txID] != nil {
	// 				for _, spentOut := range spentTxs[txID] {
	// 					if spentOut == outIdx {
	// 						continue IterOutputs
	// 					}
	// 				}
	// 			}
	// 			//确认ToAddress正确与否，正确就是我们要找的可用交易信息。
	// 			if out.ToAddressRight(address) {
	// 				unSpentTxs = append(unSpentTxs, *tx)
	// 			}
	// 		}
	// 		//检查当前交易信息是否为Base Transaction（主要是它没有input），
	// 		//如果不是就检查当前交易信息的input中是否包含目标地址，有的话就将指向的Output信息加入到spentTxs中。
	// 		if !tx.IsBase() {
	// 			for _, in := range tx.Inputs {
	// 				if in.FromAddressRight(address) {
	// 					inTxID := hex.EncodeToString(in.TxID)
	// 					spentTxs[inTxID] = append(spentTxs[inTxID], in.OutIdx)
	// 				}
	// 			}
	// 		}
	// 	}
	// }
	return unSpentTxs
}

func (bc *BlockChain) FindUTXOs(address []byte) (int, map[string]int) {
	unspentOuts := make(map[string]int)
	unspentTxs := bc.FindUnspentTransactions(address)
	accumulated := 0

Work:
	for _, tx := range unspentTxs {
		txID := hex.EncodeToString(tx.ID)
		for outIdx, out := range tx.Outputs {
			if out.ToAddressRight(address) {
				accumulated += out.Value
				unspentOuts[txID] = outIdx
				continue Work
			}
		}
	}
	return accumulated, unspentOuts
}

// 实际应用中不需要每次都要找到所有UTXO，只需找到资产总量大于本次交易转账额的一部分UTXO就行
func (bc *BlockChain) FindSpendableOutputs(address []byte, amout int) (int, map[string]int) {
	unspentOuts := make(map[string]int)
	unspentTxs := bc.FindUnspentTransactions(address)
	accumulated := 0

Work:
	for _, tx := range unspentTxs {
		txID := hex.EncodeToString(tx.ID)
		for outIdx, out := range tx.Outputs {
			if out.ToAddressRight(address) && accumulated < amout {
				accumulated += out.Value
				unspentOuts[txID] = outIdx
				if accumulated >= amout {
					break Work
				}
				continue Work // one transaction can only have one output referred to adderss
			}
		}
	}
	return accumulated, unspentOuts
}

// 在真实区块链中，一个节点会维护一个候选区块，候选区块会维持一个交易信息池（Transaction Pool），然后在挖矿时将交易池中的交易信息打包进行挖矿（PoW过程）。
func (bc *BlockChain) CreateTransaction(from_PubKey, to_HashPubKey []byte, amount int, privkey ecdsa.PrivateKey) (*transaction.Transaction, bool) {
	var inputs []transaction.TxInput
	var outputs []transaction.TxOutput

	acc, validOutputs := bc.FindSpendableOutputs(from_PubKey, amount)
	if acc < amount {
		fmt.Println("Not enough coins!")
		return &transaction.Transaction{}, false
	}
	for txid, outidx := range validOutputs {
		txID, err := hex.DecodeString(txid)
		utils.Handle(err)
		input := transaction.TxInput{txID, outidx, from_PubKey, nil}
		inputs = append(inputs, input)
	}

	outputs = append(outputs, transaction.TxOutput{amount, to_HashPubKey})
	if acc > amount {
		outputs = append(outputs, transaction.TxOutput{acc - amount, utils.PublicKeyHash(from_PubKey)})
	}
	tx := transaction.Transaction{nil, inputs, outputs}

	tx.SetID()

	tx.Sign(privkey)
	return &tx, true
}

func (bc *BlockChain) BackUTXOs(address []byte) []transaction.UTXO {
	var UTXOs []transaction.UTXO
	unspentTxs := bc.FindUnspentTransactions(address)

Work:
	for _, tx := range unspentTxs {
		for outIdx, out := range tx.Outputs {
			if out.ToAddressRight(address) {
				UTXOs = append(UTXOs, transaction.UTXO{tx.ID, outIdx, out})
				continue Work // one transaction can only have one output referred to adderss
			}
		}
	}

	return UTXOs
}

func (chain *BlockChain) GetCurrentBlock() *Block {
	var block *Block
	err := chain.Database.View(func(txn *badger.Txn) error {

		item, err := txn.Get(chain.LastHash)
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			block = DeSerializeBlock(val)
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	return block
}

func (bc *BlockChain) BackHeight() int64 {
	return bc.GetCurrentBlock().Height
}
